×
Photo profil

Baptiste Gorteau

Diplômé master MAS (mathématiques appliqués aux statistiques)
📍 Université Rennes 2

Réseaux

Création d'heat maps et analyses des tirs pour le football avec R

Introduction

Le football est très sûrement l’un des sports dans lequel la data est le plus implanté. Il est maintenant possible de trouver des données pour chaque type d’action et fait de jeu. Une part importante de ces données est accessible au grand public par l’intermédiaire d’api ou de packages. Dans ce projet, nous allons utiliser le package R worldfootballR pour visualiser et analyser les tirs des équipes dans les cinq grands championnats européens. Dans une première partie, nous allons visualiser la position des tirs des équipes à l’aide de heat maps. Ensuite, nous analyserons le ratio tirs/buts marqués dans les cinq championnats étudiés.

Dans cette étude, nous nous basons sur les données des cinq championnats européens depuis le début de cette saison (2023-24) au 30/11/2023.

Visualisation des tirs pour chaque équipe

Dans cette partie, nous allons tenter de créer une visualisation des tirs des équipes sous la forme d’heat map. Ce type de visualisation est très régulièrement utilisé car facile à comprendre et à réaliser. Ce travail va également nous permettre de démontrer la facilité de création d’heat map dès lors que nous en sommes en possession de données.

Importation des données

L’importation des données se fait à l’aide de la fonction understat_league_season_shots. Les données sont récupérées sur le site Understat.

#bundesliga <- understat_league_season_shots("Bundesliga", 2023)
#ligue1 <- understat_league_season_shots("Ligue 1", 2023)
#Liga <- understat_league_season_shots("La liga", 2023)
#Premier_League <- understat_league_season_shots("EPL", 2023)
#Serie_A <- understat_league_season_shots("Serie A", 2023)
bundesliga <- read.csv("data/bundesliga.csv")
ligue1 <- read.csv("data/ligue1.csv")
Liga <- read.csv("data/Liga.csv")
Premier_League <- read.csv("data/Premier_League.csv")
Serie_A <- read.csv("data/Serie_A.csv")

Une fois les données importées, nous les transformons en objet tibble pour pouvoir les manipuler plus facilement.

bundesliga <- as_tibble(bundesliga)
ligue1 <- as_tibble(ligue1)
Liga <- as_tibble(Liga)
Premier_League <- as_tibble(Premier_League)
Serie_A <- as_tibble(Serie_A)

Voici à quoi ressemble un de nos cinq jeux données (Chaque jeu de données possède la même structure) :

head(bundesliga)
## # A tibble: 6 × 21
##   league         id minute result          X     Y     xG player h_a   player_id
##   <chr>       <int>  <int> <chr>       <dbl> <dbl>  <dbl> <chr>  <chr>     <int>
## 1 Bundesliga 532854     22 SavedShot   0.741 0.569 0.0594 Marvi… h          4329
## 2 Bundesliga 532862     45 MissedShots 0.84  0.48  0.0255 Mitch… h            28
## 3 Bundesliga 532864     46 MissedShots 0.92  0.45  0.422  Leona… h           262
## 4 Bundesliga 532865     48 MissedShots 0.893 0.557 0.0880 Nicla… h          6098
## 5 Bundesliga 532870     62 MissedShots 0.781 0.503 0.0322 Jens … h         10734
## 6 Bundesliga 532879     91 MissedShots 0.759 0.567 0.0130 Roman… h          9069
## # ℹ 11 more variables: situation <chr>, season <int>, shotType <chr>,
## #   match_id <int>, home_team <chr>, away_team <chr>, home_goals <int>,
## #   away_goals <int>, date <chr>, player_assisted <chr>, lastAction <chr>

Voici les situations de jeux qui sont prises en compte dans nos données :

unique(bundesliga$situation)
## [1] "DirectFreekick" "OpenPlay"       "SetPiece"       "FromCorner"    
## [5] "Penalty"

Obtenir les noms des équipes

Nous sauvegardons les noms des équipes dans des vecteurs pour faciliter nos visualisations par la suite.

bundesliga_teams <- unique(bundesliga$home_team)
ligue1_teams <- unique(ligue1$home_team)
Liga_teams <- unique(Liga$home_team)
Premier_League_teams <- unique(Premier_League$home_team)
Serie_A_teams <- unique(Serie_A$home_team)
bundesliga_teams
##  [1] "Werder Bremen"          "Bayer Leverkusen"       "Wolfsburg"             
##  [4] "Hoffenheim"             "Augsburg"               "VfB Stuttgart"         
##  [7] "Borussia Dortmund"      "Union Berlin"           "Eintracht Frankfurt"   
## [10] "RasenBallsport Leipzig" "Freiburg"               "FC Cologne"            
## [13] "Bochum"                 "FC Heidenheim"          "Darmstadt"             
## [16] "Borussia M.Gladbach"    "Mainz 05"               "Bayern Munich"

Création d’une fonction de représentation d’une heat map des tirs

Pour créer la fonction de création de heat map, deux éléments sont importants :

  • Le package ggsoccer : Ce package nous permet de représenter le terrain de football grâce aux fonctions annotate_pitchet theme_pitch.
  • La fonction stat_density_2d : Permet de créer une densité 2d à partir de données X et Y.

Nous allons également représenter les buts sur la heat map à l’aide de points noirs.

# Colors for the heat map
custom_palette <- c("transparent", "green", "yellow", "orange", "red")

# Heat map function
heat_map_shots <- function(team, league, df) {
  data_home <- filter(df, df$home_team == team & df$h_a=="h")
  data_away <- filter(df, df$away_team == team & df$h_a=="a")
  data <- bind_rows(data_home, data_away)
  logo <- readPNG(sprintf("logos/%s/%s.png",league,team))
  p <- ggplot(data, aes(x=X*100, y=Y*100) ) +
  annotate_pitch(colour = "white",
                 fill   = "springgreen4",
                 limits = FALSE,
                 linewidth = 1) +
  theme_pitch() +
  theme(panel.background = element_rect(fill = "springgreen4")) +
  stat_density_2d(aes(fill = ..density..), geom = "raster", contour = FALSE) +
  scale_fill_gradientn(colors = custom_palette, guide = "none") +
  geom_point(data =filter(data, result=="Goal"), aes(X*100, Y*100), color="black")+
  coord_flip(xlim = c(49, 101)) +
  ggtitle(team)+
  theme(
    plot.title = element_text(hjust = 0.5, size = 20, face = "bold")
  )+
  annotation_custom(rasterGrob(logo, width = unit(1, "npc"), height = unit(1, "npc")), 
                      xmin= 55,xmax = 65, ymin = 85, ymax = 95)
  return(p)
}

Voici à quoi ressemble notre heat map :

heat_map_shots("Bayern Munich", "bundesliga", bundesliga)

Sur cette heat map, la variation de couleur représente la densité des tirs pris par l’équipe et les points noirs, les buts inscrits par l’équipe.

Maintenant, nous allons créer une représentation globale pour chaque championnat où nous pourrons visualiser la heat map de chaque club.

Créations de représentations pour la légende

Nous créons deux représentations qui servirons de légende pour notre représenation globale.

# Legend goals
data_legend_goal <- data.frame(
  x = c(1),
  y = c(1)
)
legend_goal <- ggplot(data_legend_goal, aes(x=x,y=y))+
  geom_point(aes(size=c(7))) +
  ylim(-1.2,1.2) +
  xlim(-4,6) +
  theme_minimal() +
  ggtitle("Goal")+
  theme(
    panel.grid = element_blank(),
    axis.title.x = element_blank(),
    axis.title.y = element_blank(),
    axis.text = element_blank(),
    plot.title = element_text(hjust = 0.5, size = 20, face = "bold"),
    legend.position = "none"
  )

# Legend shots

df <- data.frame(value = c(75),
                 group = c(1))

df_expanded <- df %>%
  rowwise() %>%
  summarise(group = group,
            value = list(0:value)) %>%
  unnest(cols = value)

legend_shots <- df_expanded %>%
  ggplot() +
  geom_tile(aes(
    x = group,
    y = value,
    fill = value,
    width = 0.9
  )) +
  coord_flip() +
  scale_fill_gradientn(colors = custom_palette, guide = "none") +
  theme(legend.position = "none") +
  xlim(0,2) +
  theme_minimal() +
  ggtitle("Shots density")+
  theme(
    panel.grid = element_blank(),
    axis.title.x = element_blank(),
    axis.title.y = element_blank(),
    axis.text = element_blank(),
    plot.title = element_text(hjust = 0.5, size = 20, face = "bold")
  )

Création de listes avec la heat map de chaque club

Pour chaque championnat, on créé une liste avec la heat map de chaque équipe à laquelle on ajoute les deux représentations de légende.

# Bundesliga
plots_bundesliga <- lapply(bundesliga_teams, function(i) {
  heat_map_shots(i, "bundesliga", bundesliga)
})
plots_bundesliga <- c(list(legend_goal, legend_shots), plots_bundesliga)
# Ligue 1
plots_ligue1 <- lapply(ligue1_teams, function(i) {
  heat_map_shots(i, "Ligue_1", ligue1)
})
plots_ligue1 <- c(list(legend_goal, legend_shots), plots_ligue1)
# Premier League
plots_Premier_League <- lapply(Premier_League_teams, function(i) {
  heat_map_shots(i, "Premier League", Premier_League)
})
plots_Premier_League <- c(list(legend_goal, legend_shots), plots_Premier_League)
# La Liga
plots_Liga <- lapply(Liga_teams, function(i) {
  heat_map_shots(i, "liga", Liga)
})
plots_Liga <- c(list(legend_goal, legend_shots), plots_Liga)
# Serie A
plots_Serie_A <- lapply(Serie_A_teams, function(i) {
  heat_map_shots(i, "Serie A", Serie_A)
})
plots_Serie_A <- c(list(legend_goal, legend_shots), plots_Serie_A)

Représentations globales

Voici les représentations globales des heat maps de chaque équipe pour nos cinq championnats.

Bundesliga

Ligue 1

Premier League

La Liga

Serie A

Ces représentations sont très intéressantes, car elles nous permettent d’avoir un aperçu de la manière dont chaque équipe attaque le but. Certaines équipes vont tirer très proche du but comme l’Atletico Madrid, d’autres vont diversifier leurs zones de tir et vont prendre beaucoup de leurs tirs en dehors de la surface comme Lecce. Ces représentations nous permettent aussi de voir que des équipes ne marquent quasiment que d’un seul côté comme Liverpool (gauche), ou au contraire, dans plusieurs zones dans et en dehors de la surface comme Naples.

Comparaison tirs/buts

Dans cette partie, nous allons observer l’efficacité de chaque équipe face au but en visualisant leur ratio tirs/buts inscrits.

Jeu de données avec les tirs et les buts de chaque équipe

La fonction renvoie un dataframe avec le nombre de tirs et de buts de chaque équipe d’un championnat.

df_sg <- function(df, teams, league){
  shots <- c()
  goals <- c()
  LogoPath <- c()
  for(team in teams){
    data_home <- filter(df, df$home_team == team & df$h_a=="h")
    data_away <- filter(df, df$away_team == team & df$h_a=="a")
    data_team <- bind_rows(data_home, data_away)
    shots <- c(shots, nrow(data_team))
    goals <- c(goals, nrow(filter(data_team, result=="Goal")))
    LogoPath <- c(LogoPath, sprintf("logos/%s/%s.png",league, team))
  }
  sg <- tibble(team = teams, shots, goals, logos = LogoPath)
  sg <-column_to_rownames(sg, var="team")
  return(sg)
}
df_sg_bundesliga <- df_sg(bundesliga, bundesliga_teams, "bundesliga")
df_sg_ligue_1 <- df_sg(ligue1, ligue1_teams, "Ligue_1")
df_sg_premier_league <- df_sg(Premier_League, Premier_League_teams, "Premier League")
df_sg_liga <- df_sg(Liga, Liga_teams, "liga")
df_sg_serie_a <- df_sg(Serie_A, Serie_A_teams, "Serie A")
head(df_sg_ligue_1)
##                     shots goals                                 logos
## Marseille             170    12           logos/Ligue_1/Marseille.png
## Nice                  171    13                logos/Ligue_1/Nice.png
## Brest                 182    13               logos/Ligue_1/Brest.png
## Paris Saint Germain   216    34 logos/Ligue_1/Paris Saint Germain.png
## Nantes                166    17              logos/Ligue_1/Nantes.png
## Clermont Foot         156     8       logos/Ligue_1/Clermont Foot.png

Fonction pour représenter les tirs et les buts

plot_sg <- function(df, league, size=.1){
  ggplot(df, aes(shots, goals)) + 
    geom_smooth(method=lm, color="red", fill="blue", se=TRUE) +
    geom_image(aes(image=logos), size=size) +
    ggtitle(sprintf("Shots / Goals comparison %s", league)) +
    theme_minimal() +
    theme(
      plot.title = element_text(hjust = 0.5, size = 20, face = "bold"),
      axis.title.x = element_text(hjust = 0.5, size = 15),
      axis.title.y = element_text(hjust = 0.5, size = 15),
      axis.line = element_line(colour = "black"),
      axis.text.x = element_text(face = "bold"),
      axis.text.y = element_text(face = "bold")
    )
}

Représentations

Bundesliga

Ligue 1

Premier League

La Liga

Serie A

Ces représentations sont très intéressantes et nous montrent déjà très logiquement que plus on prend de tirs, plus on marque. Cependant, ce n’est pas le cas de toutes les équipes. On remarque qu’en Ligue 1, Clermont et Lyon manquent de réalisme en étant dans la moyenne en termes de tirs, mais sont les deux pires équipes en ce qui concerne les buts marqués. En Premier League, Newcastle fait preuve d’un réalisme exceptionnel en étant la dixième équipe tirant le plus au but, mais la deuxième équipe ayant inscrit le plus de buts. Ce phénomène s’applique aussi en Liga avec l’Atletico Madrid et Girone. Ces deux équipes, qui sont dans le trio de tête du championnat espagnol prennent quasiment autant de tirs que le 18e du championnat, le Celta Vigo. En Allemagne et en Italie, le ratio tirs/buts et assez similaires pour presque toutes les équipes.

Comparaison entre les championnats

df_sg_all <- bind_rows(df_sg_bundesliga, df_sg_liga, df_sg_ligue_1, df_sg_premier_league, df_sg_serie_a)

À présent, lorsqu’on compare le ratio tirs/buts entre toutes les équipes des cinq championnats européens, on observe que le Bayer Leverkusen, leader de Bundesliga est l’une des équipes européennes avec le plus de réalisme. Le fait que nous n’ayons pas remarqué cette équipe dans nos premières représentations peut témoigner du réalisme face au but des équipes allemandes. Il reste tout de même nécessaire de prendre du recul sur cette représentation, car, à ce moment de la saison, le nombre de matchs jouées par les équipes diffère en fonction des championnats (Ligue 1: 12 journées, Bundesliga: 12 journées, Premier League: 13 journées, Serie A: 13 journées, La Liga: 14 journées).

Classement du réalisme

Enfin, nous pouvons réaliser un classement du réalisme face au but. Pour cela, nous allons classer les équipes en fonction de leur nombre de buts divisé par le nombre de tirs.

df_sg_all["ratio"] = df_sg_all$goals/df_sg_all$shots

Voici les 10 équipes les plus réalistes face au but.

head(arrange(df_sg_all, desc(ratio))[c("ratio")],10)
##                            ratio
## Newcastle United       0.1812865
## Bayer Leverkusen       0.1794872
## Bayern Munich          0.1757322
## Atletico Madrid        0.1734104
## RasenBallsport Leipzig 0.1676301
## Girona                 0.1666667
## VfB Stuttgart          0.1623037
## Paris Saint Germain    0.1574074
## Manchester City        0.1534884
## Hoffenheim             0.1509434

Dans ce classement, nous retrouvons en tête une partie des équipes ayant sur-performé qui ont été évoquées précédemment.

Voici les dix équipes les moins réalistes face au but.

head(arrange(df_sg_all, ratio)[c("ratio")],10)
##                       ratio
## Lyon             0.04790419
## Udinese          0.04907975
## Clermont Foot    0.05128205
## Empoli           0.05755396
## FC Cologne       0.05769231
## Alaves           0.05820106
## Verona           0.06382979
## Bochum           0.06432749
## Celta Vigo       0.06842105
## Sheffield United 0.06896552

Comme avec le haut de classement, nous retrouvons ici une partie des équipes qui ont été visées précédemment pour leur gros manque de réalisme.

Conclusion

Pour conclure, nous avons pu montrer la facilité de création et d’analyses de représentations des tirs des équipes pour les cinq grands championnats européens. Les analyses concernant le ratio tirs/buts des équipes nous a permis de comprendre que plusieurs équipes que nous n’attendions pas en haut des classements européens comme le Bayer Leverkusen, Stuttgart, Girone et Hoffenheim font preuve d’un fort réalisme face au but. Il sera intéressant de réitérer cette étude en fin de saison pour voir si les tendances se sont confirmées ou infirmées. Nous pourrions également réaliser des analyses approfondis sur la position des tirs et buts des équipes.